import jp from 'jsonpath';
import { useDataSchema, useFlattenSchema, type SchemaNode } from '@/composables/useDataSchema';
import type { IExecutionResponse, INodeUi, Schema } from '@/Interface';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import {
	NodeConnectionTypes,
	type INodeExecutionData,
	type ITaskDataConnections,
} from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { JSONSchema7 } from 'json-schema';
import { mock } from 'vitest-mock-extended';

vi.mock('@/stores/workflows.store');

describe('useDataSchema', () => {
	const getSchema = useDataSchema().getSchema;

	describe('getSchema', () => {
		test.each([
			[, { type: 'undefined', value: 'undefined', path: '' }],
			[undefined, { type: 'undefined', value: 'undefined', path: '' }],
			[null, { type: 'null', value: '[null]', path: '' }],
			['John', { type: 'string', value: 'John', path: '' }],
			['123', { type: 'string', value: '123', path: '' }],
			[123, { type: 'number', value: '123', path: '' }],
			[true, { type: 'boolean', value: 'true', path: '' }],
			[false, { type: 'boolean', value: 'false', path: '' }],
			[() => {}, { type: 'function', value: '', path: '' }],
			[{}, { type: 'object', value: [], path: '' }],
			[[], { type: 'array', value: [], path: '' }],
			[
				new Date('2022-11-22T00:00:00.000Z'),
				{ type: 'string', value: '2022-11-22T00:00:00.000Z', path: '' },
			],
			[Symbol('x'), { type: 'symbol', value: 'Symbol(x)', path: '' }],
			[1n, { type: 'bigint', value: '1', path: '' }],
			[
				['John', 1, true],
				{
					type: 'array',
					value: [
						{ type: 'string', value: 'John', key: '0', path: '[0]' },
						{ type: 'number', value: '1', key: '1', path: '[1]' },
						{ type: 'boolean', value: 'true', key: '2', path: '[2]' },
					],
					path: '',
				},
			],
			[
				{ people: ['Joe', 'John'] },
				{
					type: 'object',
					value: [
						{
							type: 'array',
							key: 'people',
							value: [
								{ type: 'string', value: 'Joe', key: '0', path: '.people[0]' },
								{ type: 'string', value: 'John', key: '1', path: '.people[1]' },
							],
							path: '.people',
						},
					],
					path: '',
				},
			],
			[
				{ 'with space': [], 'with.dot': 'test' },
				{
					type: 'object',
					value: [
						{
							type: 'array',
							key: 'with space',
							value: [],
							path: "['with space']",
						},
						{
							type: 'string',
							key: 'with.dot',
							value: 'test',
							path: "['with.dot']",
						},
					],
					path: '',
				},
			],
			[
				[
					{ name: 'John', age: 22 },
					{ name: 'Joe', age: 33 },
				],
				{
					type: 'array',
					value: [
						{
							type: 'object',
							key: '0',
							value: [
								{ type: 'string', key: 'name', value: 'John', path: '[0].name' },
								{ type: 'number', key: 'age', value: '22', path: '[0].age' },
							],
							path: '[0]',
						},
						{
							type: 'object',
							key: '1',
							value: [
								{ type: 'string', key: 'name', value: 'Joe', path: '[1].name' },
								{ type: 'number', key: 'age', value: '33', path: '[1].age' },
							],
							path: '[1]',
						},
					],
					path: '',
				},
			],
			[
				[
					{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
					{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
				],
				{
					type: 'array',
					value: [
						{
							type: 'object',
							key: '0',
							value: [
								{ type: 'string', key: 'name', value: 'John', path: '[0].name' },
								{ type: 'number', key: 'age', value: '22', path: '[0].age' },
								{
									type: 'array',
									key: 'hobbies',
									value: [
										{ type: 'string', key: '0', value: 'surfing', path: '[0].hobbies[0]' },
										{ type: 'string', key: '1', value: 'traveling', path: '[0].hobbies[1]' },
									],
									path: '[0].hobbies',
								},
							],
							path: '[0]',
						},
						{
							type: 'object',
							key: '1',
							value: [
								{ type: 'string', key: 'name', value: 'Joe', path: '[1].name' },
								{ type: 'number', key: 'age', value: '33', path: '[1].age' },
								{
									type: 'array',
									key: 'hobbies',
									value: [
										{ type: 'string', key: '0', value: 'skateboarding', path: '[1].hobbies[0]' },
										{ type: 'string', key: '1', value: 'gaming', path: '[1].hobbies[1]' },
									],
									path: '[1].hobbies',
								},
							],
							path: '[1]',
						},
					],
					path: '',
				},
			],
			[[], { type: 'array', value: [], path: '' }],
			[
				[[1, 2]],
				{
					type: 'array',
					value: [
						{
							type: 'array',
							key: '0',
							value: [
								{ type: 'number', key: '0', value: '1', path: '[0][0]' },
								{ type: 'number', key: '1', value: '2', path: '[0][1]' },
							],
							path: '[0]',
						},
					],
					path: '',
				},
			],
			[
				[
					[
						{ name: 'John', age: 22 },
						{ name: 'Joe', age: 33 },
					],
				],
				{
					type: 'array',
					value: [
						{
							type: 'array',
							key: '0',
							value: [
								{
									type: 'object',
									key: '0',
									value: [
										{ type: 'string', key: 'name', value: 'John', path: '[0][0].name' },
										{ type: 'number', key: 'age', value: '22', path: '[0][0].age' },
									],
									path: '[0][0]',
								},
								{
									type: 'object',
									key: '1',
									value: [
										{ type: 'string', key: 'name', value: 'Joe', path: '[0][1].name' },
										{ type: 'number', key: 'age', value: '33', path: '[0][1].age' },
									],
									path: '[0][1]',
								},
							],
							path: '[0]',
						},
					],
					path: '',
				},
			],
			[
				[
					{
						dates: [
							[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')],
							[new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')],
						],
					},
				],
				{
					type: 'array',
					value: [
						{
							type: 'object',
							key: '0',
							value: [
								{
									type: 'array',
									key: 'dates',
									value: [
										{
											type: 'array',
											key: '0',
											value: [
												{
													type: 'string',
													key: '0',
													value: '2022-11-22T00:00:00.000Z',
													path: '[0].dates[0][0]',
												},
												{
													type: 'string',
													key: '1',
													value: '2022-11-23T00:00:00.000Z',
													path: '[0].dates[0][1]',
												},
											],
											path: '[0].dates[0]',
										},
										{
											type: 'array',
											key: '1',
											value: [
												{
													type: 'string',
													key: '0',
													value: '2022-12-22T00:00:00.000Z',
													path: '[0].dates[1][0]',
												},
												{
													type: 'string',
													key: '1',
													value: '2022-12-23T00:00:00.000Z',
													path: '[0].dates[1][1]',
												},
											],
											path: '[0].dates[1]',
										},
									],
									path: '[0].dates',
								},
							],
							path: '[0]',
						},
					],
					path: '',
				},
			],
		])('should return the correct json schema for %s', (input, schema) => {
			expect(getSchema(input)).toEqual(schema);
		});

		it('should return the correct data when using the generated json path on an object', () => {
			const input = { people: ['Joe', 'John'] };
			const schema = getSchema(input);
			const pathData = jp.query(
				input,
				`$${((schema.value as Schema[])[0].value as Schema[])[0].path}`,
			);
			expect(pathData).toEqual(['Joe']);
		});

		it('should return the correct data when using the generated json path on a list', () => {
			const input = [
				{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
				{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
			];
			const schema = getSchema(input);
			const pathData = jp.query(
				input,
				`$${(((schema.value as Schema[])[0].value as Schema[])[2].value as Schema[])[1].path}`,
			);
			expect(pathData).toEqual(['traveling']);
		});

		it('should return the correct data when using the generated json path on a list of list', () => {
			const input = [[1, 2]];
			const schema = getSchema(input);
			const pathData = jp.query(
				input,
				`$${((schema.value as Schema[])[0].value as Schema[])[1].path}`,
			);
			expect(pathData).toEqual([2]);
		});

		it('should return the correct data when using the generated json path on a list of list of objects', () => {
			const input = [
				[
					{ name: 'John', age: 22 },
					{ name: 'Joe', age: 33 },
				],
			];
			const schema = getSchema(input);
			const pathData = jp.query(
				input,
				`$${(((schema.value as Schema[])[0].value as Schema[])[1].value as Schema[])[1].path}`,
			);
			expect(pathData).toEqual([33]);
		});

		it('should return the correct data when using the generated json path on a list of objects with a list of date tuples', () => {
			const input = [
				{
					dates: [
						[new Date('2022-11-22T00:00:00.000Z'), new Date('2022-11-23T00:00:00.000Z')],
						[new Date('2022-12-22T00:00:00.000Z'), new Date('2022-12-23T00:00:00.000Z')],
					],
				},
			];
			const schema = getSchema(input);
			const pathData = jp.query(
				input,
				`$${
					(
						(((schema.value as Schema[])[0].value as Schema[])[0].value as Schema[])[0]
							.value as Schema[]
					)[0].path
				}`,
			);
			expect(pathData).toEqual([new Date('2022-11-22T00:00:00.000Z')]);
		});
	});

	describe('filterSchema', () => {
		const filterSchema = useDataSchema().filterSchema;
		it('should correctly filter a flat schema', () => {
			const flatSchema: Schema = {
				type: 'object',
				value: [
					{
						key: 'name',
						type: 'string',
						value: 'First item',
						path: '.name',
					},
					{
						key: 'code',
						type: 'number',
						value: '1',
						path: '.code',
					},
					{
						key: 'email',
						type: 'string',
						value: 'first item@gmail.com',
						path: '.email',
					},
				],
				path: '',
			};

			expect(filterSchema(flatSchema, 'mail')).toEqual({
				path: '',
				type: 'object',
				value: [
					{
						key: 'email',
						path: '.email',
						type: 'string',
						value: 'first item@gmail.com',
					},
				],
			});
			expect(filterSchema(flatSchema, '1')).toEqual({
				path: '',
				type: 'object',
				value: [
					{
						key: 'code',
						path: '.code',
						type: 'number',
						value: '1',
					},
				],
			});
			expect(filterSchema(flatSchema, 'no match')).toEqual(null);
		});

		it('should correctly filter a nested schema', () => {
			const nestedSchema: Schema = {
				type: 'object',
				value: [
					{
						key: 'name',
						type: 'string',
						value: 'First item',
						path: '.name',
					},
					{
						key: 'code',
						type: 'number',
						value: '1',
						path: '.code',
					},
					{
						key: 'email',
						type: 'string',
						value: 'first item@gmail.com',
						path: '.email',
					},
					{
						key: 'obj',
						type: 'object',
						value: [
							{
								key: 'foo',
								type: 'object',
								value: [
									{
										key: 'nested',
										type: 'string',
										value: 'bar',
										path: '.obj.foo.nested',
									},
								],
								path: '.obj.foo',
							},
						],
						path: '.obj',
					},
				],
				path: '',
			};

			expect(filterSchema(nestedSchema, 'bar')).toEqual({
				path: '',
				type: 'object',
				value: [
					{
						key: 'obj',
						path: '.obj',
						type: 'object',
						value: [
							{
								key: 'foo',
								path: '.obj.foo',
								type: 'object',
								value: [
									{
										key: 'nested',
										path: '.obj.foo.nested',
										type: 'string',
										value: 'bar',
									},
								],
							},
						],
					},
				],
			});
			expect(filterSchema(nestedSchema, '1')).toEqual({
				path: '',
				type: 'object',
				value: [
					{
						key: 'code',
						path: '.code',
						type: 'number',
						value: '1',
					},
				],
			});
			expect(filterSchema(nestedSchema, 'no match')).toEqual(null);
		});

		it('should not filter schema with empty search', () => {
			const flatSchema: Schema = {
				type: 'object',
				value: [
					{
						key: 'name',
						type: 'string',
						value: 'First item',
						path: '.name',
					},
					{
						key: 'code',
						type: 'number',
						value: '1',
						path: '.code',
					},
					{
						key: 'email',
						type: 'string',
						value: 'first item@gmail.com',
						path: '.email',
					},
				],
				path: '',
			};

			expect(filterSchema(flatSchema, '')).toEqual(flatSchema);
		});
	});

	describe('getNodeInputData', () => {
		const getNodeInputData = useDataSchema().getNodeInputData;

		beforeEach(() => {
			setActivePinia(createTestingPinia());
		});

		afterEach(() => {
			vi.clearAllMocks();
		});

		const name = 'a';
		const makeMockData = (data: ITaskDataConnections | undefined, runDataKey?: string) => ({
			data: {
				resultData: {
					runData: {
						[runDataKey ?? name]: [
							{ data, startTime: 0, executionTime: 0, executionIndex: 0, source: [] },
						],
					},
				},
			},
		});

		const mockExecutionDataMarker = Symbol() as unknown as INodeExecutionData[];
		const Main = NodeConnectionTypes.Main;

		test.each<
			[
				[Partial<INodeUi> | null, number, number, Partial<IExecutionResponse> | null],
				ReturnType<typeof getNodeInputData>,
			]
		>([
			//
			// Null / Out of Bounds Cases
			//
			[[null, 0, 0, null], []],
			[[{ name }, 0, 0, null], []],
			[[{ name }, 0, 0, { data: undefined }], []],
			[[{ name }, 0, 0, { data: { resultData: { runData: {} } } }], []],
			[[{ name }, 0, 0, { data: { resultData: { runData: { [name]: [] } } } }], []],
			[[{ name }, 0, 0, makeMockData(undefined)], []],
			[[{ name }, 1, 0, makeMockData({})], []],
			[[{ name }, -1, 0, makeMockData({})], []],
			[[{ name }, 0, 0, makeMockData({}, 'DIFFERENT_NAME')], []],
			// getMainInputData cases
			[[{ name }, 0, 0, makeMockData({ [Main]: [] })], []],
			[[{ name }, 0, 0, makeMockData({ [Main]: [null] })], []],
			[[{ name }, 0, 1, makeMockData({ [Main]: [null] })], []],
			[[{ name }, 0, -1, makeMockData({ [Main]: [null] })], []],
			[
				[{ name }, 0, 0, makeMockData({ [Main]: [mockExecutionDataMarker] })],
				mockExecutionDataMarker,
			],
			[
				[{ name }, 0, 0, makeMockData({ [Main]: [mockExecutionDataMarker, null] })],
				mockExecutionDataMarker,
			],
			[
				[{ name }, 0, 1, makeMockData({ [Main]: [null, mockExecutionDataMarker] })],
				mockExecutionDataMarker,
			],
			[
				[
					{ name },
					0,
					1,
					makeMockData({ DIFFERENT_NAME: [], [Main]: [null, mockExecutionDataMarker] }),
				],
				mockExecutionDataMarker,
			],
			[
				[
					{ name },
					2,
					1,
					{
						data: {
							resultData: {
								runData: {
									[name]: [
										{
											startTime: 0,
											executionTime: 0,
											executionIndex: 0,
											source: [],
										},
										{
											startTime: 0,
											executionTime: 0,
											executionIndex: 1,
											source: [],
										},
										{
											data: { [Main]: [null, mockExecutionDataMarker] },
											startTime: 0,
											executionTime: 0,
											executionIndex: 2,
											source: [],
										},
									],
								},
							},
						},
					},
				],
				mockExecutionDataMarker,
			],
		])(
			'should return correct output %s',
			([node, runIndex, outputIndex, getWorkflowExecution], output) => {
				vi.mocked(useWorkflowsStore).mockReturnValue({
					...useWorkflowsStore(),
					getWorkflowExecution: getWorkflowExecution as IExecutionResponse,
				});
				expect(getNodeInputData(node as INodeUi, runIndex, outputIndex)).toEqual(output);
			},
		);
	});

	describe('getSchemaForJsonSchema', () => {
		const getSchemaForJsonSchema = useDataSchema().getSchemaForJsonSchema;

		it('should convert JSON schema to Schema type', () => {
			const jsonSchema: JSONSchema7 = {
				type: 'object',
				properties: {
					id: {
						type: 'string',
					},
					email: {
						type: 'string',
					},
					address: {
						type: 'object',
						properties: {
							line1: {
								type: 'string',
							},
							country: {
								type: 'string',
							},
						},
					},
					tags: {
						type: 'array',
						items: { type: 'string' },
					},
					workspaces: {
						type: 'array',
						items: {
							type: 'object',
							properties: {
								id: {
									type: 'string',
								},
								name: {
									type: 'string',
								},
							},
							required: ['gid', 'name', 'resource_type'],
						},
					},
				},
				required: ['gid', 'email', 'name', 'photo', 'resource_type', 'workspaces'],
			};

			expect(getSchemaForJsonSchema(jsonSchema)).toEqual({
				path: '',
				type: 'object',
				value: [
					{
						key: 'id',
						path: '.id',
						type: 'string',
						value: '',
					},
					{
						key: 'email',
						path: '.email',
						type: 'string',
						value: '',
					},
					{
						key: 'address',
						path: '.address',
						type: 'object',
						value: [
							{
								key: 'line1',
								path: '.address.line1',
								type: 'string',
								value: '',
							},
							{
								key: 'country',
								path: '.address.country',
								type: 'string',
								value: '',
							},
						],
					},
					{
						key: 'tags',
						path: '.tags',
						type: 'array',
						value: [
							{
								key: '0',
								path: '.tags[0]',
								type: 'string',
								value: '',
							},
						],
					},
					{
						key: 'workspaces',
						path: '.workspaces',
						type: 'array',
						value: [
							{
								key: '0',
								path: '.workspaces[0]',
								type: 'object',
								value: [
									{
										key: 'id',
										path: '.workspaces[0].id',
										type: 'string',
										value: '',
									},
									{
										key: 'name',
										path: '.workspaces[0].name',
										type: 'string',
										value: '',
									},
								],
							},
						],
					},
				],
			});
		});
	});
});

describe('useFlattenSchema', () => {
	describe('flattenSchema', () => {
		it('flattens a schema', () => {
			const schema: Schema = {
				path: '',
				type: 'object',
				value: [
					{
						key: 'obj',
						path: '.obj',
						type: 'object',
						value: [
							{
								key: 'foo',
								path: '.obj.foo',
								type: 'object',
								value: [
									{
										key: 'nested',
										path: '.obj.foo.nested',
										type: 'string',
										value: 'bar',
									},
								],
							},
						],
					},
				],
			};
			expect(
				useFlattenSchema().flattenSchema({
					schema,
				}).length,
			).toBe(3);
		});

		it('items ids should be unique', () => {
			const { flattenSchema } = useFlattenSchema();
			const schema: Schema = {
				path: '',
				type: 'object',
				value: [
					{
						key: 'index',
						type: 'number',
						value: '0',
						path: '.index',
					},
				],
			};
			const node1Schema = flattenSchema({ schema, expressionPrefix: '$("First Node")', depth: 1 });
			const node2Schema = flattenSchema({ schema, expressionPrefix: '$("Second Node")', depth: 1 });

			expect(node1Schema[0].id).not.toBe(node2Schema[0].id);
		});
	});

	describe('flattenMultipleSchemas', () => {
		it('should handle empty data', () => {
			const { flattenMultipleSchemas } = useFlattenSchema();

			const result = flattenMultipleSchemas(
				[
					mock<SchemaNode>({
						node: { name: 'Test Node' },
						isDataEmpty: true,
						schema: { type: 'object', value: [] },
					}),
				],
				vi.fn(),
			);
			expect(result).toHaveLength(2);
			expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
			expect(result[1]).toEqual(
				expect.objectContaining({ type: 'empty', key: 'emptyData', level: 1 }),
			);
		});

		it('should handle unexecuted nodes', () => {
			const { flattenMultipleSchemas } = useFlattenSchema();

			const result = flattenMultipleSchemas(
				[
					mock<SchemaNode>({
						node: { name: 'Test Node' },
						isNodeExecuted: false,
						schema: { type: 'object', value: [] },
					}),
				],
				vi.fn(),
			);
			expect(result).toHaveLength(2);
			expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
			expect(result[1]).toEqual(
				expect.objectContaining({ type: 'empty', key: 'executeSchema', level: 1 }),
			);
		});

		it('should handle empty schema', () => {
			const { flattenMultipleSchemas } = useFlattenSchema();

			const result = flattenMultipleSchemas(
				[
					mock<SchemaNode>({
						node: { name: 'Test Node' },
						isDataEmpty: false,
						hasBinary: false,
						schema: { type: 'object', value: [] },
					}),
				],
				vi.fn(),
			);
			expect(result).toHaveLength(2);
			expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
			expect(result[1]).toEqual(
				expect.objectContaining({ type: 'empty', key: 'emptySchema', level: 1 }),
			);
		});

		it('should handle empty schema with binary', () => {
			const { flattenMultipleSchemas } = useFlattenSchema();

			const result = flattenMultipleSchemas(
				[
					mock<SchemaNode>({
						node: { name: 'Test Node' },
						isDataEmpty: false,
						hasBinary: true,
						schema: { type: 'object', value: [] },
					}),
				],
				vi.fn(),
			);
			expect(result).toHaveLength(2);
			expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
			expect(result[1]).toEqual(
				expect.objectContaining({ type: 'empty', key: 'emptySchemaWithBinary', level: 1 }),
			);
		});

		it('should flatten node schemas', () => {
			const { flattenMultipleSchemas } = useFlattenSchema();
			const schema: Schema = {
				path: '',
				type: 'object',
				value: [
					{
						key: 'obj',
						path: '.obj',
						type: 'object',
						value: [
							{
								key: 'foo',
								path: '.obj.foo',
								type: 'object',
								value: [
									{
										key: 'nested',
										path: '.obj.foo.nested',
										type: 'string',
										value: 'bar',
									},
								],
							},
						],
					},
				],
			};

			const result = flattenMultipleSchemas(
				[
					mock<SchemaNode>({
						node: { name: 'Test Node' },
						isDataEmpty: false,
						hasBinary: false,
						preview: false,
						schema,
					}),
					mock<SchemaNode>({
						node: { name: 'Test Node' },
						isDataEmpty: false,
						hasBinary: false,
						preview: false,
						schema,
					}),
				],
				vi.fn(),
			);
			expect(result).toHaveLength(10);
			expect(result.filter((item) => item.type === 'header')).toHaveLength(2);
			expect(result.filter((item) => item.type === 'item')).toHaveLength(8);
			expect(result).toMatchSnapshot();
		});
	});
});
